package edu.kit.aifb.ldsbench.generator.util2;

import dk.ange.octave.OctaveEngine;
import dk.ange.octave.OctaveEngineFactory;
import dk.ange.octave.type.OctaveDouble;

import edu.kit.aifb.ldsbench.generator.opt2.IOptimizationSolver;
import edu.kit.aifb.ldsbench.generator.opt2.OptProblemInstance;
import edu.kit.aifb.ldstream.stat.distrib.BinomialDistribution;
import edu.kit.aifb.ldstream.stat.distrib.ContinuousUniformDistribution;
import edu.kit.aifb.ldstream.stat.distrib.DiscreteUniformDistribution;
import edu.kit.aifb.ldstream.stat.distrib.ExponentialDistribution;
import edu.kit.aifb.ldstream.stat.distrib.IDistribution;
import edu.kit.aifb.ldstream.stat.distrib.NormalDistribution;
import edu.kit.aifb.ldstream.stat.distrib.PoissonDistribution;

public class Octave implements IDistributionValues, IOptimizationSolver{

	private static OctaveEngineFactory factory = new OctaveEngineFactory();
	
	private static final int numIterations = 50;
	private static final int timeLimit = 10; //in seconds
	
	private OctaveEngine octaveInstance;

	public Octave(){
		this.octaveInstance = factory.getScriptEngine();
		octaveInstance.setErrorWriter(null);
		octaveInstance.setWriter(null);
	}


	public double getDistributionValue(IDistribution distrib){
		return getDistributionValue(distrib, 1)[0];
	}


	public double[] getDistributionValue(IDistribution distribution, int numValues){
		if(distribution == null){
			throw new IllegalArgumentException("Distribution must not be null.");
		}
		if(numValues < 1){
			throw new IllegalArgumentException("Number of distribution values must be greater than 0.");
		}

		double[] values = new double[numValues];

		double param1, param2;
		// getting the underlying distribution type
		switch(distribution.getDistributionType()){
		case IDistribution.BINOMIAL : 
			BinomialDistribution bd = (BinomialDistribution) distribution;
			param1 = bd.getSamples();
			param2 = bd.getP();
			this.octaveInstance.eval("rnds = binornd(" + param1 +"," + param2 + "," + numValues + ", 1 )");
			values = this.octaveInstance.get(OctaveDouble.class, "rnds").getData();
			//System.out.println("binomial distribution with n=" + param1 + " p=" + param2 );
			break;
		case IDistribution.EXPONENTIAL:
			ExponentialDistribution exd = (ExponentialDistribution) distribution;
			param1 = exd.getLamda();
			this.octaveInstance.eval("rnds = exprnd(" + param1 +"," + numValues + ", 1 )");
			values = this.octaveInstance.get(OctaveDouble.class, "rnds").getData();
			break;
		case IDistribution.NORMAL: 
			NormalDistribution nd = (NormalDistribution) distribution;
			param1 = nd.getMean();
			param2 = nd.getSd();
			this.octaveInstance.eval("rnds = normrnd(" + param1 +"," + param2 + "," + numValues + ", 1 )");
			values = this.octaveInstance.get(OctaveDouble.class, "rnds").getData();
			break;
		case IDistribution.POISSON:
			PoissonDistribution pd = (PoissonDistribution) distribution;
			param1 = pd.getLambda();
			this.octaveInstance.eval("rnds = poissrnd(" + param1 +"," + numValues + ", 1 )");
			values = this.octaveInstance.get(OctaveDouble.class, "rnds").getData();
			break;
		case IDistribution.UNIFORM_CONTINUOUS:
			ContinuousUniformDistribution cud = (ContinuousUniformDistribution) distribution;
			// to avoid same upper and lower limit which works for ks test but not for the rnd generation
			param1 = cud.getMin() - 0.0001d;
			param2 = cud.getMax();
			this.octaveInstance.eval("rnds = unifrnd(" + param1 +"," + param2 + "," + numValues + ", 1 )");
			values = this.octaveInstance.get(OctaveDouble.class, "rnds").getData();
			break;
		case IDistribution.UNIFORM_DISCRETE:
			DiscreteUniformDistribution dud = (DiscreteUniformDistribution) distribution;
			// to avoid same upper and lower limit which works for ks test but not for the rnd generation
			param1 = dud.getMin() - 0.0001d;
			param2 = dud.getMax();
			this.octaveInstance.eval("rnds = round(unifrnd(" + param1 +"," + param2 + "," + numValues + ", 1 ))");
			values = this.octaveInstance.get(OctaveDouble.class, "rnds").getData();
			break;
		}
		// resetting the octave instance
		this.octaveInstance.eval("clear");
		return values;

	}

	public void shutdown(){
		this.octaveInstance.destroy();
	}

	@Override
	public double[] sendOptimizationProblem(OptProblemInstance opi) {

		/*
		 * Building A
		 */
		StringBuilder a = new StringBuilder("A = [");
		double[] array = opi.getA();
		for(int i = 0; i < array.length; i++){
			a.append(array[i]);
			// i + 1 to exclude ";" in the first position
			if( (i + 1) % opi.getaDim() == 0){
				a.append(";");
			}else{
				a.append(",");
			}
		}
		a.append("]");
		this.octaveInstance.eval(a.toString());

		/*
		 * Building b,ctype
		 */

		StringBuilder b = new StringBuilder("b = [");
		StringBuilder ctype = new StringBuilder("ctype = \"");

		double[] bArray = opi.getB();
		byte[] ctypeArray = opi.getCtype();

		for(int i = 0; i < bArray.length; i++){
			b.append(bArray[i]);
			ctype.append(getCtype(ctypeArray[i]));
			if(i == bArray.length - 1){
				b.append("]");
				ctype.append("\"");
			}else{
				b.append(",");
			}
		}

		this.octaveInstance.eval(b.toString());
		this.octaveInstance.eval(ctype.toString());

		// lower bound array, could be empty 

		StringBuilder lb = new StringBuilder("lb = [");
		double[] lbArray = opi.getLb();

		for(int i = 0; i < lbArray.length; i++){
			lb.append(lbArray[i]);
			if(i < lbArray.length - 1){
				lb.append(",");
			}
		}
		lb.append("]");
		this.octaveInstance.eval(lb.toString());

		// upper bound array, could be empty

		double[] ubArray = opi.getUb();
		StringBuilder ub = new StringBuilder("ub = [");

		for(int i = 0; i < ubArray.length; i++){
			ub.append(ubArray[i]);
			if(i < ubArray.length - 1){
				ub.append(",");
			}
		}
		ub.append("]");
		this.octaveInstance.eval(ub.toString());
		/*
		 * Building vartype and c
		 */
		StringBuilder vartype = new StringBuilder("vartype = \"");
		StringBuilder c = new StringBuilder("c = [");
		double[] cArray = opi.getC();

		for(int i = 0; i < cArray.length; i++){
			c.append(cArray[i]);
			vartype.append("I");
			if(i == cArray.length - 1){
				c.append("]");
				vartype.append("\"");
			}else{
				c.append(",");
			}
		}
		this.octaveInstance.eval(vartype.toString());
		this.octaveInstance.eval(c.toString());

		byte s;
		if(opi.isMinimize()){
			s = 1;
		}else{
			s = -1;
		}
		this.octaveInstance.eval("s = " + s);

		// no output
		this.octaveInstance.eval("param.msglev = 0");
		this.octaveInstance.eval("param.itlim = " + numIterations);
		this.octaveInstance.eval("param.tmlim = " + timeLimit);

		// sending the optimization problem to octave
	//	System.out.println("gplk");
		this.octaveInstance.eval("[xopt, fopt, status, extra] = " 
				+ "glpk(c, A, b, lb, ub, ctype, vartype, s, param)");
	//	System.out.println("gplk2");
		int status = (int) this.octaveInstance.get(OctaveDouble.class, "status").get(1,1);
		printStatus(status);

		boolean success = succeeded(status);
		double[] xopt = null;
		// Retrieve the result if optimization succeeded
		if(success){
			double fopt =  this.octaveInstance.get(OctaveDouble.class, "fopt").get(1,1);
		//	System.out.println("optimized coherence value: " + fopt);

			// getting the result
			xopt = this.octaveInstance.get(OctaveDouble.class, "xopt").getData();

			/*
			System.out.print("Optimal solution: [ ");
			for(int i = 0; i < xopt.length; i++){
				System.out.print(xopt[i] + ", ");
			}
			System.out.println("]");
			*/
		}
		this.octaveInstance.eval("clear");

		return xopt;
	}

	private boolean succeeded(int statusCode){
		if(statusCode == 180 || statusCode == 181 || 
				statusCode == 151 || statusCode == 171){
			return true;
		}
		return false;
	}

	private String getCtype(byte typeCode){
		switch(typeCode){
		case OptProblemInstance.CONSTRAINT_WITH_LOWER_BOUND : return "L";
		case OptProblemInstance.CONSTRAINT_WITH_UPPER_BOUND : return "U";
		case OptProblemInstance.CONSTRAINT_WITH_EQUALITY : return "S";
		case OptProblemInstance.CONSTRAINT_WITH_BOTH_BOUNDS : return "D";
		}

		return null;
	}

	private void printStatus(int statusCode){
		switch(statusCode){
		case 171: System.out.println("solution is integer optimal") ; break; 
		case 173:  System.out.println("solution is not feasible for integers") ; break;
		case 180: System.out.println("solution is optimal") ; break;
		case 181: System.out.println("solution is feasible") ; break;
		case 182:  System.out.println("solution is infeasible") ; break;
		case 183:  System.out.println("no feasible solution") ; break;
		case 184:  System.out.println("no unbounded solution") ; break;
		case 204:  System.out.println("unable to start the search") ; break;
		case 207:  System.out.println("iterations exhausted") ; break;
		case 208: System.out.println("time limit exhausted") ; break;
		default : System.out.println("optimization status code: " + statusCode + " see octave manual"); 
		}
	}
}
